package liuyang.bigeventserver.modules.security.filter;

import liuyang.bigeventserver.modules.security.jwt.JwtTokenExtractor;
import liuyang.bigeventserver.modules.security.jwt.JwtTokenService;
import liuyang.bigeventserver.modules.security.jwt.vo.PrincipalInfo;
import liuyang.bigeventserver.modules.security.vo.CachedInfo;
import liuyang.bigeventserver.modules.security.vo.SysUserDetails;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;

/**
 * 一句话描述：从token中解析userId，再从缓存（redis）中拿出用户信息（LoginUser extends UserDetails）放入SecurityContextHolder。
 *
 * 很重要！
 *
 * 参考：
 *  3. Thor
 *  https://www.bilibili.com/video/BV1Fd4y1k7rq/?p=40&spm_id_from=pageDriver&vd_source=8bd7b24b38e3e12c558d839b352b32f4
 *  1. 三更草堂方案
 *  2. 飞浪方案
 * 注：Spring Security OAuth2中有对JWT的支持，但在一个相对简单的前后端分离应用当中，自定义实现即可。
 *
 * @author liuyang
 * @since 2022/1/28
 *        2024/2/27 更换为jakarta包
 *        2024/3/1  fix token超时无法重新登录的bug。
 */
@Component
@AllArgsConstructor
@Slf4j
public class JwtOncePerRequestFilter extends OncePerRequestFilter {

    private final JwtTokenService jwtTokenService;
    private final RedisTemplate<String, Object> redisTemplate3;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String token = JwtTokenExtractor.extract(request);
        if (StringUtils.isNotEmpty(token) && StringUtils.isNotBlank(token)) {
            // 1. 从JWT Token中解析出用户名
            final PrincipalInfo info = jwtTokenService.verify(token);// TODO 还有可能抛出验证异常
            // 2. 从缓存中找出用户信息
            //final Collection<? extends GrantedAuthority> authorities = (Collection<? extends GrantedAuthority>)redisTemplate3.opsForValue().get(info.getSubject());
            final CachedInfo cachedInfo = (CachedInfo) redisTemplate3.opsForValue().get(info.getSubject());
            final String cachedJwt = cachedInfo.getJwt();
            log.debug("username(from token) = {} authorities(from redis) = {}", info.getSubject(), cachedInfo.getAuthorities());

            // 20240303 add 如果携带的jwt和缓存的jwt不一致，则需要重新登录
            /*
            if (null == cachedInfo.getAuthorities()) {
                // TODO 需要重新认证 抛出异常
                throw new RuntimeException("用户已注销 需要重新登录");// 这个如何实现？
            }
             */
            if (null == cachedInfo) {
                throw new RuntimeException("用户已注销 需要重新登录");
            }
            if (!token.equals(cachedJwt)) {
                throw new RuntimeException("用户令牌已失效 需要重新登录");
            }

            SysUserDetails sysUserDetails = new SysUserDetails(Long.parseLong(info.getId()), info.getSubject(), "", cachedInfo.getAuthorities());
            // 3. 把权限信息放回安全上下文
            //UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(info.getSubject(), null, authorities);
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(sysUserDetails, null, cachedInfo.getAuthorities());
            usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 解决JWT Token过期时
     * @param request current HTTP request
     * @return
     * @throws ServletException
     */
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        boolean flag = false;// 放行标志
        if (request.getRequestURI().contains("/login")) {
            log.debug("放行/login请求");
            flag = true;
            return flag;
        }
        // TODO logout也放行？ 20240303 想到的问题
        //return super.shouldNotFilter(request);
        return flag;
    }
}
